package ar.com.cubika.trainees.bean.transportes.vehiculos;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;

import ar.com.cubika.trainees.bean.transportes.actividades.Mantenimiento;
import ar.com.cubika.trainees.bean.transportes.actividades.PlanContinuacionDeViajeLargo;
import ar.com.cubika.trainees.bean.transportes.actividades.PlanDiario;
import ar.com.cubika.trainees.bean.transportes.actividades.PlanEnMantenimiento;
import ar.com.cubika.trainees.bean.transportes.actividades.PlanEnViaje;
import ar.com.cubika.trainees.bean.transportes.actividades.PlanSinActividad;
import ar.com.cubika.trainees.bean.transportes.actividades.Viaje;
import ar.com.cubika.trainees.collections.MethodEqualsObjectPredicate;
import ar.com.cubika.trainees.collections.ParametrizedMethodPredicate;
import ar.com.cubika.trainees.date.DateUtils;
import ar.com.cubika.trainees.exceptions.BusinessException;

/**
 * Representa los vehiculos que usa la empresa para hacer viajes.
 * @author Carlos
 */
public abstract class Vehiculo {

    private String patente;
    private Set<Viaje> viajes;
    private Set<Mantenimiento> mantenimientos;
    
    /**
     * Crea un nuevo vehiculo 
     */
    protected Vehiculo(String patente) {
        this.patente = patente;
        this.clearActividades();
    }

    
    /**
     * La carga maxima que puede llevar el vehiculo en un viaje, 
     * en kilos
     */
    public abstract long getCargaMaxima();
    
    /**
     * El tiempo que le lleva al vehiculo hacer un determinado viaje,
     * en horas
     */
    public abstract double getTiempo(Viaje viaje);
    
    /**
     * El consumo de combustible calculado para un viaje, medido en litros
     */
    public abstract double getConsumoCombustible(Viaje viaje);
    
    /**
     * Devuelve <code>true</code> si el vehiculo puede hacer  
     * un determinado viaje, dadas las restricciones de tiempo, carga
     * y disponibilidad.
     * 
     * @param viaje el viaje en analisis
     */
    public boolean puedeHacer(Viaje viaje) {
    	return (this.getCargaMaxima() >= viaje.getCarga()) &&
    			(this.getTiempo(viaje) <= viaje.getTiempoMaximo()) &&
    			this.tieneDisponibilidad(viaje);
    }

    /**
     * Devuelve <code>true</code> si el vehiculo puede hacer  
     * un determinado mantenimiento.
     * 
     * @param viaje el mantenimiento en analisis
     */
    public boolean puedeHacer(Mantenimiento mant) {
    	Iterator diasIt = mant.getPeriodo().diasIterator();
    	while (diasIt.hasNext()) {
			Date dia = (Date) diasIt.next();
			if (!this.getPlan(dia).isLibre()) {
				return false;
			}
		}
    	return true;
    }

    protected boolean tieneDisponibilidad(Viaje viaje) {
    	PlanDiario planDelDia = this.getPlan(viaje.getFechaPartida());
    	double tiempoViaje = this.getTiempo(viaje); 
    	if (tiempoViaje <= 12.0) {
    		return planDelDia.getTiempoOcupado() + tiempoViaje <= 12;
    	} else {
    		return 
    				planDelDia.isLibre() && 
    				this.getPlan(
    						DateUtils.addDays(viaje.getFechaPartida(),1)).
    						isLibre();
    	}
    }
    
    /**
     * El plan del vehiculo para un dia.
     */
    public PlanDiario getPlan(Date fecha) {
    	PlanDiario result;
    	Collection<Viaje> viajes;
    	Viaje viajeLargoDeAyer;
    	Mantenimiento mant;
    	if (!(viajes  = this.getViajes(fecha)).isEmpty()) {
    		PlanEnViaje enViaje = new PlanEnViaje(this, fecha);
    		enViaje.addViajes(viajes);
    		result = enViaje;
    	} else if ((mant = this.getMantenimiento(fecha)) != null) {
    		PlanEnMantenimiento enMant = new PlanEnMantenimiento(this, fecha);
    		enMant.setMantenimiento(mant);
    		result = enMant;
    	} else if ((viajeLargoDeAyer = this.getViajeLargo(
    			DateUtils.addDays(fecha, -1))) != null) {
    		PlanContinuacionDeViajeLargo contViaje = 
    			new PlanContinuacionDeViajeLargo(this, fecha);
    		contViaje.setViaje(viajeLargoDeAyer);
    		result = contViaje;
    	} else {
    		result = new PlanSinActividad(this, fecha);
    	}
    	return result;
    }
    
    
    /**
     * El viaje largo (de mas de 12 horas) para una fecha, null
     * si no hay ninguno.
     * @param date la fecha en cuestion
     */
	protected Viaje getViajeLargo(Date fecha) {
		Collection viajes = this.getViajes(fecha);
		if (viajes.size() == 1) {
			Viaje viaje = (Viaje)viajes.iterator().next();
			return viaje.getTiempoEstimado() >= 12.0 ? viaje : null;
		} else {
			return null;
		}
	}


	/**
     * Los viajes del vehiculo para una fecha
     * @see Vehiculo#getPlan(Date)
     */
    protected Collection<Viaje> getViajes(Date fecha) {
    	Collection select = CollectionUtils.select(
    			this.getViajes(), 
    			new MethodEqualsObjectPredicate(fecha, "getFechaPartida"));
        return (Collection<Viaje>)select;
	}

    protected Mantenimiento getMantenimiento(Date fecha) {
    	return (Mantenimiento)CollectionUtils.find(
    			this.getMantenimientos(), 
    			new ParametrizedMethodPredicate(fecha, "incluyeFecha"));
	}


	/**
     * La patente que identifica al vehiculo 
     */
    public String getPatente() {
        return patente;
    }
    
    public void setPatente(String patente) {
        this.patente = patente;
    }
    
    /**
     * Solo para ser usado dese Viaje, se le asigna un viaje 
     * al vehiculo.
     * @see Viaje#asignar(Vehiculo)
     */
    public void addViaje(Viaje viaje) {
        this.viajes.add(viaje);
    }
    
    /**
     * Solo para ser usado desde Viaje, se le quita el viaje que
     * habia sido previamente asignado al vehiculo
     * @see Viaje#desasignar()
     */
    public void removeViaje(Viaje viaje) {
    	this.viajes.remove(viaje);
    }
    
    /**
     * Agrega un mantenimiento al vehiculo.
     * @throws ar.com.cubika.trainees.exceptions.BusinessException 
     * 		si el mantenimiento no es para el vehiculo.
     */
    public synchronized void addMantenimiento(Mantenimiento mant) {
    	if (!mant.getVehiculo().equals(this)) {
    		throw new BusinessException(
    				"El mantenimiento " + mant + 
    				" no es del vehiculo " + this);
    	}
    	this.mantenimientos.add(mant);
    }
    
    /**
     * Quita un mantenimiento del vehiculo.
     * @throws ar.com.cubika.trainees.exceptions.BusinessException 
     * 		si el mantenimiento no esta entre los del vehiculo.
     */
    public synchronized void removeMantenimiento(Mantenimiento mant) {
    	if (!this.mantenimientos.contains(mant)) {
    		throw new BusinessException(
    				"El vehiculo " + this + 
    				" no incluye al mantenimiento " + mant);
    	}
    	this.mantenimientos.remove(mant);
    }
    
   
    /**
     * Los viajes que tiene asignado el vehiculo
     */
    public Collection<Viaje> getViajes() {
        return new ArrayList<Viaje>(this.viajes);
    }

    /**
     * Los mantenimientos que tiene planificado el vehiculo
     */
	public Collection<Mantenimiento> getMantenimientos() {
		return new ArrayList<Mantenimiento>(this.mantenimientos);
	}


    /**
     * Me limpian todos los viajes asignados - para test
     */
    public synchronized void clearActividades() {
        this.viajes = new HashSet<Viaje>();
        this.mantenimientos = new HashSet<Mantenimiento>();
    }

    public String toString() {
        return this.getTipoVehiculo() + " " + this.getPatente();
    }

    public String getTipoVehiculo() {
      String name = this.getClass().getName();
      return name.substring(name.lastIndexOf(".") + 1).toLowerCase();
    }


    /**
     * Devuelve el consumo total de los viajes asignados a este vehiculo.
     */
    public double getConsumoTotalViajesAsignados() {
        double total = 0;
        for (Iterator viajesIt = this.getViajes().iterator(); viajesIt.hasNext();) {
            Viaje viaje = (Viaje) viajesIt.next();
            total += this.getConsumoCombustible(viaje);
        }
        return total;
    }
}
